Skip to content

feat: data-attachment wrapper (Phase 2) - #13

Merged
KP2048 merged 2 commits into
1.21.xfrom
feature-data-attachment
Aug 8, 2026
Merged

feat: data-attachment wrapper (Phase 2)#13
KP2048 merged 2 commits into
1.21.xfrom
feature-data-attachment

Conversation

@KP2048

@KP2048 KP2048 commented Aug 7, 2026

Copy link
Copy Markdown
Member

Phase 2 of the pre-release storage/GUI plan: data-attachment wrapper

Wraps Common Storage Lib's DataManager so Archie mods can attach persistent (and optionally
synced) data to holders they don't own the class of — Entity, BlockEntity, ItemStack, and
(NeoForge only) ServerLevel — via a stateless, reusable object, complementing NBTHolder's
per-instance-owned field storage.

New API — net.kernelpanicsoft.archie.serialization.AttachmentRegistry/ArchieDataAttachment

object MyAttachments : AttachmentRegistry(MyMod.MOD_ID) {
    val mana by intAttachment(sync = true, default = { 0 })
}
var Entity.mana by MyAttachments.mana

// mod init:
MyAttachments.init()
  • attachment(serializer, sync, copyOnDeath, itemComponent, default) (plus a reified variant and
    primitive convenience wrappers - intAttachment, stringAttachment, etc. - mirroring
    NBTHolder's field helpers).
  • sync (reactive push to tracking players on every write) and itemComponent (vanilla
    DataComponentType-backed, rides normal item replication instead) are two genuinely different
    mechanisms - confirmed against both platforms' real DataManagerBuilderImpl source that
    itemComponent needs a client codec regardless of sync, since CSL's own builder breaks at
    registration time otherwise. Archie's wrapper always supplies one when needed, so this can't
    bite a consumer.
  • Used directly (MyAttachments.mana.get(holder)/.set/.has/.remove/.modify) or as an
    extension-property delegate, as above.

A real platform quirk, found and documented (not dismissed)

get() on an unset Entity/BlockEntity/ServerLevel value silently creates and persists the
default - both Fabric's getAttachedOrCreate and NeoForge's getData write through on a miss.
That means has() can only tell "never touched" apart from "read once" if called before the
first get(). First surfaced as two real GameTest failures on Fabric; verified against Fabric's
actual attachment source rather than assumed, then documented in ArchieDataAttachment's KDoc and
docs/serialization.md, and the test suite now checks has() before get() accordingly.
ItemStack/itemComponent holders don't have this quirk.

Also confirmed (from CSL's real source, both platforms): ServerLevel sync is genuinely NeoForge-only

  • Fabric's updateTarget dispatch has no Level case at all.

A real bug caught before it shipped

Returning ArchieDataAttachment<T> directly as a PropertyDelegateProvider's own delegate type
(since it itself implements ReadWriteProperty<Any?, T> so it can also back an extension
property) made val mana by intAttachment(...) unwrap straight through to T instead of binding
mana's type to the attachment object itself. Isolated via a standalone repro before touching the
real files; fixed by wrapping it in a plain ReadOnlyProperty, matching the existing pattern
NBTHolderImpl.itemField/fluidField/energyField already use for the same reason.

Verification

New DataAttachmentTests/DataAttachmentTestFixtures in Archie-Test, using vanilla
Entity/BlockEntity/ItemStack fixtures throughout (attachments are generic across holder kind,
unlike Phase 1's capability lookups, so no custom registered fixture type is needed). All 8 pass
under a real fabric-test:runGametest run:

[GameTest] PASS archie:dataattachmenttests.testblockentityattachmentroundtrip
[GameTest] PASS archie:dataattachmenttests.testextensionpropertydelegateroundtrip
[GameTest] PASS archie:dataattachmenttests.testgetreturnsdefaultbeforeset
[GameTest] PASS archie:dataattachmenttests.testitemcomponentattachmentroundtripsonitemstack
[GameTest] PASS archie:dataattachmenttests.testmodifyappliesfunctionandpersists
[GameTest] PASS archie:dataattachmenttests.testsetgethasremoveroundtrip
[GameTest] PASS archie:dataattachmenttests.testunexposeditemcomponentattachmentthrowsonitemstack
[GameTest] PASS archie:dataattachmenttests.testunsupportedholderthrowsillegalargumentexception

neoforge-test:runGametest currently registers zero real tests for any mod in this environment
(GameTestHooks: Enabled Gametest Namespaces: [], before any of this PR's code runs) - a
pre-existing NeoForge GameTest task/environment gap, not something introduced here (Phase 1 never
exercised that task either). NeoForge-specific behavior here is instead verified directly against
CSL's real NeoForge source (DataManagerImpl/DataManagerBuilderImpl), not just assumed
symmetric with Fabric.

Independent of Phase 1 (still open, unmerged) per the approved plan - this branches directly off
origin/1.21.x, so its test file uses plain fail(...) checks rather than Phase 1's not-yet-merged
public assertion helpers.

Part of the three-feature pre-release plan (capability lookup / data attachments / item-backed
container menus); Phase 3 to follow in a separate PR.

🤖 Generated with Claude Code

Add AttachmentRegistry/ArchieDataAttachment, wrapping Common Storage Lib's
DataManager to attach data to holders Archie doesn't own the class of -
Entity, BlockEntity, ItemStack, and (NeoForge only) ServerLevel - via a
stateless, reusable object rather than NBTHolder's per-instance-owned
storage. Declare via `AttachmentRegistry(modId) { val mana by intAttachment(...) }`,
use directly (`.get`/`.set`/`.has`/`.remove`/`.modify`) or as a delegate for
an extension property (`var Entity.mana by MyAttachments.mana`).

`sync` (reactive push to tracking players - Entity/BlockEntity on both
loaders, ServerLevel NeoForge-only) and `itemComponent` (vanilla
DataComponentType-backed, rides normal item replication instead) are kept
as two distinct, independently-toggleable mechanisms, matching what CSL's
real DataManagerBuilder actually requires under the hood (itemComponent
needs a client codec regardless of sync, confirmed against both platforms'
real DataManagerBuilderImpl source - passing null there breaks at
registration time).

Verified against CSL's actual source (both platforms' DataManagerBuilder/
DataManagerImpl, not just the bytecode signatures) rather than guessing:
- get()/has() exception behavior per holder kind, confirmed exact.
- A real, previously-undocumented platform quirk: get() on an unset
  Entity/BlockEntity/ServerLevel value silently creates *and persists* the
  default (Fabric's getAttachedOrCreate, NeoForge's getData both write
  through on a miss) - has() can only tell "never touched" from "read once"
  if called before the first get(). Documented in ArchieDataAttachment's
  KDoc and docs/serialization.md; the new GameTest suite checks has()
  before get() accordingly, after first hitting exactly this false
  failure and confirming it against real Fabric behavior rather than
  dismissing it as a test bug.
- ServerLevel sync is genuinely NeoForge-only (Fabric's updateTarget has
  no Level case at all).

Also fixed a real Kotlin delegate-provider bug caught before it shipped:
returning ArchieDataAttachment<T> directly as a PropertyDelegateProvider's
own delegate type (since it itself implements ReadWriteProperty<Any?, T>)
made `val mana by intAttachment(...)` unwrap straight through to T instead
of binding mana's type to the attachment object - wrapping it in a plain
ReadOnlyProperty (matching NBTHolderImpl.itemField/fluidField/energyField's
existing pattern) fixes it. Isolated and confirmed via a standalone repro
before touching the real files.

New DataAttachmentTests/DataAttachmentTestFixtures in Archie-Test, using
vanilla Entity/BlockEntity/ItemStack fixtures throughout (attachments are
generic across holder kind, unlike Phase 1's capability lookups, so no
custom registered fixture type is needed). All 8 pass under a real
fabric-test:runGametest run.

Independent of Phase 1 (still open, unmerged) per the approved plan -
this branches off origin/1.21.x directly, so its test file uses plain
fail(...) checks rather than Phase 1's not-yet-merged public assertion
helpers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@KP2048

KP2048 commented Aug 8, 2026

Copy link
Copy Markdown
Member Author

@copilot resolve the merge conflicts in this pull request

Co-authored-by: KP2048 <39203202+KP2048@users.noreply.github.com>
@KP2048
KP2048 marked this pull request as ready for review August 8, 2026 15:35
Copilot AI lite review requested due to automatic review settings August 8, 2026 15:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@KP2048
KP2048 merged commit 6029836 into 1.21.x Aug 8, 2026
3 of 5 checks passed
@KP2048
KP2048 deleted the feature-data-attachment branch August 8, 2026 15:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants